/*
Copyright The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controller

import (
	"context"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/tools/record"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1"
	"sigs.k8s.io/kueue/cmd/experimental/kueue-populator/pkg/constants"
)

// Event is a simplified representation of a Kubernetes event for testing purposes.
type Event struct {
	EventType string
	Reason    string
	Message   string
}

func TestKueuePopulatorReconciler(t *testing.T) {
	cases := map[string]struct {
		clusterQueues       []*kueue.ClusterQueue
		namespaces          []*corev1.Namespace
		localQueues         []*kueue.LocalQueue // Existing LocalQueues to be set up before reconciliation
		globalNsSelectorStr string
		localQueueName      string
		req                 reconcile.Request
		wantError           error
		wantLQs             []kueue.LocalQueue
		wantEvents          []Event
	}{
		"should create a default LocalQueue if the namespace matches cq and global selector is nil": {
			clusterQueues: []*kueue.ClusterQueue{
				{
					ObjectMeta: metav1.ObjectMeta{Name: "cq"},
					Spec: kueue.ClusterQueueSpec{
						NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"dep": "eng"}},
					},
				},
			},
			namespaces: []*corev1.Namespace{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "ns",
						Labels: map[string]string{"dep": "eng"},
					},
				},
			},
			localQueueName: "default-lq",
			req:            reconcile.Request{NamespacedName: types.NamespacedName{Name: "ns"}},
			wantLQs: []kueue.LocalQueue{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "default-lq",
						Namespace: "ns",
						Labels:    map[string]string{constants.AutoGeneratedLabel: "true"},
					},
					Spec: kueue.LocalQueueSpec{
						ClusterQueue: "cq",
					},
				},
			},
			wantEvents: []Event{
				{
					EventType: corev1.EventTypeNormal,
					Reason:    "LocalQueueCreated",
					Message:   "Created LocalQueue default-lq in namespace ns",
				},
			},
		},
		"should create a default LocalQueue if the namespace matches both cq and global selectors": {
			clusterQueues: []*kueue.ClusterQueue{
				{
					ObjectMeta: metav1.ObjectMeta{Name: "cq"},
					Spec: kueue.ClusterQueueSpec{
						NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"dep": "eng"}},
					},
				},
			},
			namespaces: []*corev1.Namespace{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "ns",
						Labels: map[string]string{"dep": "eng", "team": "alpha"},
					},
				},
			},
			globalNsSelectorStr: "team=alpha",
			localQueueName:      "default-lq",
			req:                 reconcile.Request{NamespacedName: types.NamespacedName{Name: "ns"}},
			wantLQs: []kueue.LocalQueue{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "default-lq",
						Namespace: "ns",
						Labels:    map[string]string{constants.AutoGeneratedLabel: "true"},
					},
					Spec: kueue.LocalQueueSpec{
						ClusterQueue: "cq",
					},
				},
			},
			wantEvents: []Event{
				{
					EventType: corev1.EventTypeNormal,
					Reason:    "LocalQueueCreated",
					Message:   "Created LocalQueue default-lq in namespace ns",
				},
			},
		},
		"should do nothing if clusterQueue is being deleted": {
			clusterQueues: func() []*kueue.ClusterQueue {
				now := metav1.Now()
				return []*kueue.ClusterQueue{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name:              "cq",
							DeletionTimestamp: &now,
							Finalizers:        []string{"finalizer"},
						},
						Spec: kueue.ClusterQueueSpec{
							NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"dep": "eng"}},
						},
					},
				}
			}(),
			namespaces: []*corev1.Namespace{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "ns",
						Labels: map[string]string{"dep": "eng"},
					},
				},
			},
			localQueueName: "default-lq",
			req:            reconcile.Request{NamespacedName: types.NamespacedName{Name: "ns"}},
			wantLQs:        []kueue.LocalQueue{},
			wantEvents:     nil,
		},
		"should do nothing if namespace is being deleted": {
			clusterQueues: []*kueue.ClusterQueue{
				{
					ObjectMeta: metav1.ObjectMeta{Name: "cq"},
					Spec: kueue.ClusterQueueSpec{
						NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"dep": "eng"}},
					},
				},
			},
			namespaces: func() []*corev1.Namespace {
				now := metav1.Now()
				return []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name:              "ns",
							Labels:            map[string]string{"dep": "eng"},
							DeletionTimestamp: &now,
							Finalizers:        []string{"finalizer"},
						},
					},
				}
			}(),
			localQueueName: "default-lq",
			req:            reconcile.Request{NamespacedName: types.NamespacedName{Name: "ns"}},
			wantLQs:        []kueue.LocalQueue{},
			wantEvents:     nil,
		},
		"should preserve existing LocalQueue if clusterQueue is being deleted": {
			clusterQueues: func() []*kueue.ClusterQueue {
				now := metav1.Now()
				return []*kueue.ClusterQueue{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name:              "cq",
							DeletionTimestamp: &now,
							Finalizers:        []string{"finalizer"},
						},
						Spec: kueue.ClusterQueueSpec{
							NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"dep": "eng"}},
						},
					},
				}
			}(),
			namespaces: []*corev1.Namespace{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "ns",
						Labels: map[string]string{"dep": "eng"},
					},
				},
			},
			localQueues: []*kueue.LocalQueue{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "default-lq",
						Namespace: "ns",
						Labels:    map[string]string{constants.AutoGeneratedLabel: "true"},
					},
					Spec: kueue.LocalQueueSpec{
						ClusterQueue: "cq",
					},
				},
			},
			localQueueName: "default-lq",
			req:            reconcile.Request{NamespacedName: types.NamespacedName{Name: "ns"}},
			wantLQs: []kueue.LocalQueue{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "default-lq",
						Namespace: "ns",
						Labels:    map[string]string{constants.AutoGeneratedLabel: "true"},
					},
					Spec: kueue.LocalQueueSpec{
						ClusterQueue: "cq",
					},
				},
			},
			wantEvents: nil,
		},
		"should do nothing if no namespace matches": {
			clusterQueues: []*kueue.ClusterQueue{
				{
					ObjectMeta: metav1.ObjectMeta{Name: "cq"},
					Spec: kueue.ClusterQueueSpec{
						NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"dep": "eng"}},
					},
				},
			},
			namespaces: []*corev1.Namespace{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "ns",
						Labels: map[string]string{"dep": "another"},
					},
				},
			},
			localQueueName: "default-lq",
			req:            reconcile.Request{NamespacedName: types.NamespacedName{Name: "ns"}},
			wantLQs:        []kueue.LocalQueue{},
			wantEvents:     nil,
		},
		"should do nothing if a LocalQueue with the same name already exists and points to the same ClusterQueue": {
			clusterQueues: []*kueue.ClusterQueue{
				{
					ObjectMeta: metav1.ObjectMeta{Name: "cq"},
					Spec: kueue.ClusterQueueSpec{
						NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"dep": "eng"}},
					},
				},
			},
			namespaces: []*corev1.Namespace{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "ns",
						Labels: map[string]string{"dep": "eng"},
					},
				},
			},
			localQueues: []*kueue.LocalQueue{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "default-lq",
						Namespace: "ns",
						Labels:    map[string]string{constants.AutoGeneratedLabel: "true"},
					},
					Spec: kueue.LocalQueueSpec{
						ClusterQueue: "cq",
					},
				},
			},
			localQueueName: "default-lq",
			req:            reconcile.Request{NamespacedName: types.NamespacedName{Name: "ns"}},
			wantLQs: []kueue.LocalQueue{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "default-lq",
						Namespace: "ns",
						Labels:    map[string]string{constants.AutoGeneratedLabel: "true"},
					},
					Spec: kueue.LocalQueueSpec{
						ClusterQueue: "cq",
					},
				},
			},
			wantEvents: nil,
		},
		"should emit a warning event if a LocalQueue with the same name already exists for a different ClusterQueue": {
			clusterQueues: []*kueue.ClusterQueue{
				{
					ObjectMeta: metav1.ObjectMeta{Name: "cq"},
					Spec: kueue.ClusterQueueSpec{
						NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"dep": "eng"}},
					},
				},
			},
			namespaces: []*corev1.Namespace{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "ns",
						Labels: map[string]string{"dep": "eng"},
					},
				},
			},
			localQueues: []*kueue.LocalQueue{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "default-lq",
						Namespace: "ns",
					},
					Spec: kueue.LocalQueueSpec{
						ClusterQueue: "some-other-cq",
					},
				},
			},
			localQueueName: "default-lq",
			req:            reconcile.Request{NamespacedName: types.NamespacedName{Name: "ns"}},
			wantLQs: []kueue.LocalQueue{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "default-lq",
						Namespace: "ns",
					},
					Spec: kueue.LocalQueueSpec{
						ClusterQueue: "some-other-cq",
					},
				},
			},
			wantEvents: []Event{
				{
					EventType: corev1.EventTypeWarning,
					Reason:    "LocalQueueExists",
					Message:   "Skipping LocalQueue creation in namespace ns, a LocalQueue with name default-lq already exists",
				},
			},
		},
		"should not create a default LocalQueue if the namespace doesn't match the global selector": {
			clusterQueues: []*kueue.ClusterQueue{
				{
					ObjectMeta: metav1.ObjectMeta{Name: "cq"},
					Spec: kueue.ClusterQueueSpec{
						NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"dep": "eng"}},
					},
				},
			},
			namespaces: []*corev1.Namespace{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "ns",
						Labels: map[string]string{"dep": "eng"},
					},
				},
			},
			globalNsSelectorStr: "team=alpha",
			localQueueName:      "default-lq",
			req:                 reconcile.Request{NamespacedName: types.NamespacedName{Name: "ns"}},
			wantLQs:             []kueue.LocalQueue{},
			wantEvents:          nil,
		},
		"should create a default LocalQueue if the ClusterQueue has a nil NamespaceSelector": {
			clusterQueues: []*kueue.ClusterQueue{
				{
					ObjectMeta: metav1.ObjectMeta{Name: "cq"},
				},
			},
			namespaces: []*corev1.Namespace{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "ns",
					},
				},
			},
			localQueueName: "default-lq-nil",
			req:            reconcile.Request{NamespacedName: types.NamespacedName{Name: "ns"}},
			wantLQs: []kueue.LocalQueue{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "default-lq-nil",
						Namespace: "ns",
						Labels:    map[string]string{constants.AutoGeneratedLabel: "true"},
					},
					Spec: kueue.LocalQueueSpec{
						ClusterQueue: "cq",
					},
				},
			},
			wantEvents: []Event{
				{
					EventType: corev1.EventTypeNormal,
					Reason:    "LocalQueueCreated",
					Message:   "Created LocalQueue default-lq-nil in namespace ns",
				},
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			scheme := runtime.NewScheme()
			_ = corev1.AddToScheme(scheme)
			_ = kueue.AddToScheme(scheme)

			builder := fake.NewClientBuilder().WithScheme(scheme)

			for _, cq := range tc.clusterQueues {
				builder.WithObjects(cq)
			}
			for _, ns := range tc.namespaces {
				builder.WithObjects(ns)
			}
			for _, lq := range tc.localQueues {
				builder.WithObjects(lq)
			}

			k8sclient := builder.Build()
			recorder := record.NewFakeRecorder(10)

			var opts []KueuePopulatorReconcilerOption
			if tc.globalNsSelectorStr != "" {
				selector, err := labels.Parse(tc.globalNsSelectorStr)
				if err != nil {
					t.Fatalf("failed to parse globalNsSelector: %v", err)
				}
				opts = append(opts, WithNamespaceSelector(selector))
			}
			opts = append(opts, WithLocalQueueName(tc.localQueueName))

			reconciler := NewKueuePopulatorReconciler(
				k8sclient,
				recorder,
				opts...,
			)

			ctx := context.Background()
			_, gotErr := reconciler.Reconcile(ctx, tc.req)
			if diff := cmp.Diff(tc.wantError, gotErr, cmpopts.EquateErrors()); diff != "" {
				t.Errorf("Reconcile() error (-want +got):\n%s", diff)
			}

			var lqList kueue.LocalQueueList
			if err := k8sclient.List(ctx, &lqList); err != nil {
				t.Errorf("Failed to list LocalQueues: %v", err)
			}

			if diff := cmp.Diff(tc.wantLQs, lqList.Items, cmpopts.IgnoreFields(metav1.ObjectMeta{}, "ResourceVersion")); diff != "" {
				t.Errorf("LocalQueues (-want +got):\n%s", diff)
			}

			close(recorder.Events)
			var gotEvents []Event
			for event := range recorder.Events {
				e := Event{}
				parts := strings.SplitN(event, " ", 3)
				if len(parts) < 2 {
					t.Fatalf("Invalid event format: %q", event)
				}
				e.EventType = parts[0]
				e.Reason = parts[1]
				if len(parts) > 2 {
					e.Message = strings.Trim(parts[2], "`")
				}
				gotEvents = append(gotEvents, e)
			}

			if diff := cmp.Diff(tc.wantEvents, gotEvents); diff != "" {
				t.Errorf("Events (-want +got):\n%s", diff)
			}
		})
	}
}
